package jmine.tec.web.servlet.rest;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import jmine.tec.web.servlet.rest.exception.HttpErrorException;

import org.apache.commons.lang.StringUtils;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import bancosys.tec.persist.dao.BeanNotFoundException;
import bancosys.tec.utils.date.Date;

/**
 * Subclasses devem sobrescrever esta classe para criar servicos rest que nao dependam do mecanismo de renderizacao. Idealmente, deveriamos
 * usar proxy de transacao pois normalmente subclasses desta vao precisar sempre de controle transacional
 * 
 * @author takeshi
 */
@Transactional(isolation = Isolation.DEFAULT, propagation = Propagation.REQUIRED, rollbackFor = { IOException.class,
        BeanNotFoundException.class, HttpErrorException.class })
public abstract class RestImplementor extends AbstractRestSupport {

    static final Object ALREAD_HANDLED = new Object();

    /**
     * Faz a operacao de get em uma transacao somente leitura
     * 
     * @param request o {@link HttpServletRequest}
     * @param response o {@link HttpServletResponse}
     * @return Object
     * @throws IOException e
     * @throws HttpErrorException vai setar o erro http
     * @throws BeanNotFoundException vai forcar um 404
     */
    @Transactional(isolation = Isolation.DEFAULT, propagation = Propagation.REQUIRED, rollbackFor = { IOException.class,
            BeanNotFoundException.class, HttpErrorException.class }, readOnly = true)
    public Object get(final HttpServletRequest request, final HttpServletResponse response) throws IOException, BeanNotFoundException,
            HttpErrorException {
        final String identification = this.extractIdentification(request);
        Object r;
        if (identification == null) {
            r = this.get(request);
        } else {
            r = this.get(identification, request);
        }
        return this.preSerialize(r, this.parameterMap(request));
    }

    /**
     * Prepara um objeto ou colecao para 'serializacao'
     * 
     * @param r Object
     * @param map {@link Map}
     * @return Object
     * @throws HttpErrorException e
     */
    protected Object preSerialize(final Object r, final Map<String, String[]> map) throws HttpErrorException {
        if (r instanceof Collection<?>) {
            return this.shrinkAll((Collection<?>) r, map);
        } else {
            return this.prepareToSerialize(r, map);
        }
    }

    /**
     * Faz a operacao de post. Este metodo deveria criar ou atualizar entidades
     * 
     * @param request o {@link HttpServletRequest}
     * @param response o {@link HttpServletResponse}
     * @return Object
     * @throws IOException e
     * @throws HttpErrorException vai setar o erro http
     * @throws BeanNotFoundException vai forcar um 404
     */
    public Object post(final HttpServletRequest request, final HttpServletResponse response) throws IOException, BeanNotFoundException,
            HttpErrorException {
        final String identification = this.extractIdentification(request);
        Object r;
        if (identification == null) {
            r = this.post(request);
        } else {
            r = this.post(identification, request);
        }
        return this.prepareToSerialize(r, this.parameterMap(request));
    }

    /**
     * Faz a operacao de put. Este metodo deveria apenas atualizar uma entidade
     * 
     * @param request o {@link HttpServletRequest}
     * @param response o {@link HttpServletResponse}
     * @return Object
     * @throws IOException e
     * @throws HttpErrorException vai setar o erro http
     * @throws BeanNotFoundException vai forcar um 404
     */
    public Object put(final HttpServletRequest request, final HttpServletResponse response) throws IOException, BeanNotFoundException,
            HttpErrorException {
        final String identification = this.extractIdentification(request);
        Object r;
        if (identification == null) {
            r = this.put(request);
        } else {
            r = this.put(identification, request);
        }
        return this.prepareToSerialize(r, this.parameterMap(request));
    }

    /**
     * Faz a operacao de delete. Este metodo deveria apagar uma entidade
     * 
     * @param request o {@link HttpServletRequest}
     * @param response o {@link HttpServletResponse}
     * @return Object
     * @throws IOException e
     * @throws HttpErrorException vai setar o erro http
     * @throws BeanNotFoundException vai forcar um 404
     */
    public Object delete(final HttpServletRequest request, final HttpServletResponse response) throws IOException, HttpErrorException,
            BeanNotFoundException {
        final String identification = this.extractIdentification(request);
        Object r;
        if (identification == null) {
            r = this.delete(request);
        } else {
            r = this.delete(identification, request);
        }
        return this.prepareToSerialize(r, this.parameterMap(request));
    }

    /**
     * Prepara um objeto para serializacao / renderizacao para o usuario
     * 
     * @param e Object
     * @param parameters {@link Map}
     * @return Object
     * @throws HttpErrorException e
     */
    protected Object prepareToSerialize(final Object e, final Map<String, String[]> parameters) throws HttpErrorException {
        return e;
    }

    /**
     * "Encolhe" objetos para serem devolvidos como uma lista
     * 
     * @param collection {@link Collection}
     * @param parameters {@link Map}
     * @return {@link Collection}
     * @throws HttpErrorException e
     */
    protected Object shrinkAll(final Collection<?> collection, final Map<String, String[]> parameters) throws HttpErrorException {
        final List<Object> list = new ArrayList<Object>(collection.size());
        for (final Object e2 : collection) {
            list.add(this.shrinkOne(e2, parameters));
        }
        return list;
    }

    /**
     * Encolhe um objeto para ser guardado numa colecao.
     * 
     * @param e Object
     * @param parameters {@link Map}
     * @return Object
     * @throws HttpErrorException e
     */
    protected Object shrinkOne(final Object e, final Map<String, String[]> parameters) throws HttpErrorException {
        return e;
    }

    /**
     * Obtém o parâmetro armazenado no mapa com a chave.
     * 
     * @param map map
     * @param key key
     * @return String
     */
    protected String getParameter(final Map<String, String[]> map, final String key) {
        final String[] value = map.get(key);
        if (value == null || value.length == 0) {
            return null;
        } else {
            try {
                return URLDecoder.decode(value[0], "UTF-8");
            } catch (final UnsupportedEncodingException e) {
                throw new IllegalArgumentException("Não foi possível decodificar parâmetro '" + key + "'.");
            }
        }
    }

    /**
     * Efetua o parse do valor de data.
     * 
     * @param value value
     * @return Date
     * @throws HttpErrorException vai setar o erro http
     */
    protected Date toDate(final String value) throws HttpErrorException {
        if (StringUtils.isBlank(value)) {
            return null;
        } else {
            final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            try {
                return new Date(sdf.parse(value).getTime());
            } catch (final ParseException e) {
                throw new HttpErrorException(HttpServletResponse.SC_NOT_ACCEPTABLE, value);
            }
        }
    }
}
